// jDataTable <?php
/**
 * jDataTable jQuery Plugin v1.0.1
 *     by Olivier St-Laurent
 *
 * Usage :
 *      <table class="jDataTable">
 *          <thead>
 *              <tr>
 *                  <th data-table-foot-1="Total" data-table-foot-2="Average Price"></th>
 *                  <th data-table-sort="text" data-table-filter data-table-concat>Item Name</th>
 *                  <th data-table-sort="number" data-table-foot-1="%total%" data-table-foot-2="%average%">Price</th>
 *              </tr>
 *          </thead>
 *          <tbody>
 *              <tr>
 *                  <td></td>
 *                  <td>Potato Bag</td>
 *                  <td data-table-value="4"> 4.00 $ </td>
 *              </tr>
 *              <tr>
 *                  <td></td>
 *                  <td>Orange Box</td>
 *                  <td data-table-value="10.5"> 10.50 $ </td>
 *              </tr>
 *          </tbody>
 *          <tfoot>
 *              <tr data-table-foot="1">
 *                  <th></th>
 *                  <td></td>
 *                  <td></td>
 *              </tr>
 *              <tr data-table-foot="2">
 *                  <th></th>
 *                  <td></td>
 *                  <td></td>
 *              </tr>
 *          </tfoot>
 *      </table>
 *
 *      <script>
 *          $('.jDataTable').jDataTable().render();
 *      </script>
 *
 *      <style type="text/css">
 *          .table-row-filtered,
 *          .table-row-concat,
 *          .table-col-hide,
 *          .table-row-hide
 *          {
 *              display: none;
 *          }
 *          .table-sort-arrow {
 *              font-size: 16px;
 *              cursor: pointer;
 *              color: #ccc;
 *          }
 *          .table-sort-arrow:before {
 *              content: "\25b2";
 *          }
 *          .table-sort-arrow-active {
 *              color: #000;
 *          }
 *          .table-sort-arrow-asc:before {
 *              content: "\25b2";
 *          }
 *          .table-sort-arrow-desc:before {
 *              content: "\25bc";
 *          }
 *          .table-filter-select {
 *
 *          }
 *          .table-hide {
 *
 *          }
 *          .table-concat {
 *
 *          }
 *          .table-concat-active {
 *
 *          }
 *      </style>
 *
 *
 * Documentation :
 *
 *   THEAD TH/TD Attributes :
 *
 *      data-table-sort       Sort rows using this column (text|number|date).
 *                              Supported string formats for date values :
 *                                  YYYY-MM-DD
 *                                  YYYY-MM-DD HH:mm:ss
 *
 *      data-table-hide       Hides the entire column
 *
 *      data-table-filter     Filter this column with a given list separated by '|' (ex: "value1|value2|value3").
 *                            If no value is provided, it will use all the different values in each row for this column.
 *
 *      data-table-filter-default-text      Default text to show in the options for the filter (Ex. "All")
 *
 *      data-table-concat     Concatenate the rows with the same value for this column.
 *
 *      data-table-onconcat   formated value when concatenating this column for different values
 *                            Possible replacement strings for concatenated rows :
 *                                  %first% First value as-is
 *                                  %last% Last value as-is
 *                                  %total% Total value with two digits after decimal
 *                                  %totalint% Total using integer value
 *                                  %average% Average value with two degits after decimal
 *                                  %max% Maximum value as-is
 *                                  %min% Minimum value as-is
 *                                  %count% Number of concatenated rows
 *                            Default value if not defined : %last%
 *
 *      data-table-foot-*     Show a formated value in the TFOOT for this column,
 *                            Possible replacement strings for calculated value for all visible rows :
 *                                  %total% Total value with two digits after decimal
 *                                  %totalint% Total using integer value
 *                                  %average% Average value with two degits after decimal
 *                                  %max% Maximum value as-is
 *                                  %min% Minimum value as-is
 *                                  %count% Number of visible rows
 *
 *   TFOOT TR Attributes :
 *
 *      data-table-foot       Specify what this TR is used for in the TFOOT,
 *                            The value is the number that will be used in the THEAD TH attributes
 *                            Ex:
 *                                  THEAD TH : data-table-foot-1="%total%"
 *                                  TFOOT TR : data-table-foot="1"
 *
 *   TBODY TH/TD Attributes :
 *
 *      data-table-value      The raw value to use for calculations, if none provided, will use innerHTML of TD.
 *
 *
 *
 *   External Methods examples :
 *      $('.jDataTable').jDataTableObject() // Access the object
 *                                         .filter(columnIndex, 'value'); // Filter rows by column value (needs refresh)
 *                                         .sort(columnIndex, true); // Sort Desc if second param is true (needs refresh)
 *                                         .concat(columnIndex, true); // Concatenate if second param is true (needs refresh)
 *                                         .limit(nbRows); // Limit the number of rows (0 or null for unlimited) (needs refresh)
 *                                         .hide(columnIndex, true); // Hide column if second param is true
 *                                         .refreshRows(); // Recalculate visible rows and sort them
 *                                         .renderFooter(); // Recalculate the footer
 *                                         .init(); // Initialise table columns
 *                                         .refresh(); // Refresh the table, ex: after calling sort(), filter(), concat(), limit()
 *
 *
 *///?>

var jDataTableDefaultOptions = {
    // Selectors
    headRowSelector : 'thead tr:last',
    headColsSelector : 'th, td',
    footRowSelector : 'tfoot tr',
    footColsSelector : 'th, td',
    bodyRowSelector : 'tbody tr',
    bodyColsSelector : 'th, td',
    ///////////////////////////////
    // Row Classes
    filteredClass : 'table-row-filtered',
    concatClass : 'table-row-concat',
    concatResultClass : 'table-row-concatresult',
    hideRowClass : 'table-row-hide',
    hideColClass : 'table-col-hide',
    ///////////////////////////////
    // Header filters/buttons
    // Sort
    sortingArrowElement : 'i',
    sortingArrowClass : 'table-sort-arrow',
    sortingArrowClassActive : 'table-sort-arrow-active',
    sortingArrowClassAsc : 'table-sort-arrow-asc',
    sortingArrowClassDesc : 'table-sort-arrow-desc',
    addSorting : function(containerElement, sortElement, colIndex) {
        $(containerElement).append(sortElement);
    },
    // Filter
    filterSelectClass : 'table-filter-select',
    addFilter : function(containerElement, selectElement, colIndex) {
        $(containerElement).append(selectElement);
    },
    // Hide
    hideBtnElement : 'button',
    hideBtnClass : 'table-hide',
    addHideBtn : function(containerElement, btnElement, colIndex) {
        btnElement.innerHTML = 'Hide';
        $(containerElement).append(btnElement);
    },
    // Concat
    concatBtnElement : 'button',
    concatBtnClass : 'table-concat',
    concatBtnClassActive : 'table-concat-active',
    addConcatBtn : function(containerElement, btnElement, colIndex) {
        btnElement.innerHTML = 'Concat';
        $(containerElement).append(btnElement);
    },
    concatResult : function(colElement, value, concatValue, colIndex, rowElems) {
        return value;
    },
    // Footer
    footerResult : function(colElement, value, colIndex, rowElems) {
        return value;
    },
    ///////////////////////////////
    // Events
    onAfterFilterChange : function(selectedValue, selectElement, colIndex) { // This is called before "onAfterRefresh"
    },
    onAfterConcat : function(active, concatBtnElement, colIndex) { // This is called before "onAfterRefresh"
    },
    onAfterSort : function(desc, sortBtnElement, colIndex) { // This is called before "onAfterRefresh"
    },
    onAfterRender : function(tableObject) {
    },
    onAfterRefresh : function(tableObject) { // Last event to be triggered once the table is fully ready
    }
    ///////////////////////////////
};
var jDataTableObjects = new Array();
(function($) {
    // jQuery Table Initializer
    $.fn.jDataTable = function(options) {
        var jDataTableObj = null;
        $(this).filter('table').each(function() {
            if (!$(this).is('[data-table-bound]')) {
                jDataTableObj = new $.jDataTable($(this), options, jDataTableObjects.length);
                jDataTableObjects.push(jDataTableObj);
            } else {
                var index = $(this).attr('data-table-bound');
                if (!jDataTableObjects[index]) {
                    jDataTableObjects[index] = new $.jDataTable($(this), options, index);
                }
            }
        });
        return jDataTableObj; // Return last bound object for practicality purposes
    };
    // jQuery Table Accessor
    $.fn.jDataTableObject = function() {
        return jDataTableObjects[$(this).attr('data-table-bound')];
    };
    // Table Function Object
    $.jDataTable = function(table, options, index) {
        // Object init
        var self = this;
        this.table = table;
        this.index = index;

        // Merge options
        if (typeof options === 'undefined') {
            options = {};
        }
        options = $.extend({}, jDataTableDefaultOptions, options);
        this.options = options;

        // Variable initialisation
        this.cols = null;
        this.footer = null;
        this.sortContainer = null;
        this.concatColumn = false;
        this.limitRows = 0;
        this.originalElements = new Array();

        //////////////////////////////////////////////////////////////////////////////
        // Main methods

        this.bind = function() {
            table.attr('data-table-bound', self.index);
            self.init();
            addControls();
            return this;
        };

        this.unbind = function() {
            jDataTableObjects[self.index] = false;
            removeControls();
            delete(this);
            return;
        };

        this.init = function() {
            self.cols = new Array();
            self.footer = new Array();
            self.sortContainer = $(document.createElement('div'));

            table.find(self.options.headRowSelector).each(function(headRowIndex) {
                $(this).find(self.options.headColsSelector).each(function(colIndex) {
                    self.cols[colIndex] = {
                        elem : $(this),
                        sort : getAttr(this, 'data-table-sort', ['text', 'number', 'date']),
                        filter : getAttr(this, 'data-table-filter', function(value) {
                            if (value === '') {
                                return true;
                            }
                            return value;
                        }),
                        filterDefaultText : getAttr(this, 'data-table-filter-default-text', null, ''),
                        concat : getAttr(this, 'data-table-concat', true),
                        onconcat : getAttr(this, 'data-table-onconcat', null, '%last%'),
                        hide : getAttr(this, 'data-table-hide', true),
                        filterSelectElem : null,
                        sortButtonElem : null,
                        concatButtonElem : null,
                        hideButtonElem : null,
                        footers : []
                    };
                    if (self.cols[colIndex].sort) {
                        var el = document.createElement('i'); // Dummy element
                        $(el).attr('data-sort-col', colIndex);
                        $(el).attr('data-sort-desc', 0);
                        self.sortContainer.append(el);
                    }
                });
            });
            table.find(self.options.footRowSelector).each(function(footRowIndex) {
                var footerId = getAttr(this, 'data-table-foot');
                self.footer[footRowIndex] = {
                    elem : $(this),
                    footerId : footerId,
                    cols : []
                };
                $(this).find(self.options.footColsSelector).each(function(colIndex) {
                    self.footer[footRowIndex].cols[colIndex] = $(this);
                    self.cols[colIndex].footers[footerId] = {
                        elem : $(this),
                        val : getAttr(self.cols[colIndex].elem, 'data-table-foot-' + footerId)
                    };
                });
            });
            return this;
        };

        //////////////////////////////////////////////////////////////////////////////
        // Helper functions

        /**
         * Get a specific attribute for the selected column element
         * @param DOM Element
         * @param string Attribute name
         * @param true|Array List of possible values
         * @param mixed Value to return when the attribute is not set
         * @returns mixed the resulting value
         */
        function getAttr(elem, attr, possibleValues, defaultUnsetValue) {
            var value = $(elem).attr(attr);
            if (typeof value === 'undefined') {
                if (typeof defaultUnsetValue !== 'undefined') {
                    return defaultUnsetValue;
                }
                return false;
            } else {
                if (possibleValues === true) {
                    return true;
                } else {
                    if (!possibleValues) {
                        return value;
                    } else {
                        if (typeof possibleValues === 'object') {
                            for (var i in possibleValues) {
                                if (possibleValues[i] === value) {
                                    return value;
                                }
                            }
                            return possibleValues[0];
                        } else {
                            if (typeof possibleValues === 'function') {
                                return possibleValues(value);
                            } else {
                                return possibleValues;
                            }
                        }
                    }
                }
                return true;
            }
        }

        /**
         * Get the value of a specific TD/TH Element
         * @param DOM Element
         * @returns string The final value
         */
        function getVal(elem) {
            var value = $(elem).attr('data-table-value');
            if (typeof value === 'undefined') {
                value = $(elem).html();
            }
            return value;
        }

        /**
         * Parse Date to Timestamp
         * @param string Date String
         *      Supported date string formats :
         *          YYYY-MM-DD
         *          YYYY-MM-DD HH:mm:ss
         *
         * @returns long timestamp
         */
        function getTimestamp(timeStr) {
            if (timeStr.match(/^\d{4}-\d{2}-\d{2}$/g)) {
                timeStr = '' + timeStr + ' 00:00:00';
            }
            if (timeStr.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/g)) {
                var d = timeStr.match(/\d+/g);
                return +new Date(d[0], d[1] - 1, d[2], d[3], d[4], d[5]);
            }
            return 0;
        }

        /**
         * Add control elements (filter selectboxs and sorting arrows) to the table
         */
        function addControls() {
            for (var colIndex in self.cols) {
                var col = self.cols[colIndex];
                var container = col.elem;

                // We save the original parent element state so we can easily revert when removing controls
                self.originalElements.push({elem : $(container)[0], originalContents : '' + $(container).html()});

                if (col.sort) {
                    // Add arrows for Sorting

                    var el = document.createElement(self.options.sortingArrowElement);
                    el.className = self.options.sortingArrowClass;
                    $(el).attr('data-table-col-index', colIndex);

                    // Action when clicking on Sort
                    $(el).on('click', function() {
                        self.sort($(this).attr('data-table-col-index'), $(this).hasClass(self.options.sortingArrowClassAsc));
                        self.refresh();
                    });

                    // Adding a reference to the element in the object
                    self.cols[colIndex].sortButtonElem = $(el);

                    // Adding the element to the TH container using a customizable method in the options
                    self.options.addSorting(container, el, colIndex);
                }

                if (col.filter) {
                    // Add SelectBox for filters

                    var options = new Array();
                    if (col.filter === true) {
                        // If filter values are not specified, use all different values from all rows.
                        self.table.find(self.options.bodyRowSelector).each(function(bodyRowIndex) {
                            $(this).find(self.options.bodyColsSelector).each(function(bodyColIndex) {
                                if (+colIndex === +bodyColIndex) {
                                    var value = getVal(this).toLowerCase();
                                    if ($.inArray(value, options) === -1) {
                                        options.push(value);
                                    }
                                }
                            });
                        });
                    } else {
                        // Use specified filter values
                        options = col.filter.split('|');
                    }

                    // Sort filter list
                    options = options.sort();

                    // Creating the selectbox
                    var select = document.createElement('select');
                    select.className = self.options.filterSelectClass;
                    $(select).attr('data-table-col-index', colIndex);

                    // Adding the default/first option
                    var defaultOption = document.createElement('option');
                    defaultOption.value = '';
                    defaultOption.innerHTML = col.filterDefaultText;
                    $(select).append(defaultOption);

                    // Add filter options to selectbox
                    for (var i in options) {
                        var o = document.createElement('option');
                        o.value = options[i];
                        o.innerHTML = options[i];
                        $(select).append(o);
                    }

                    // Action when changing a filter
                    $(select).on('change', function() {
                        self.filter($(this).attr('data-table-col-index'), this.value);
                        self.refresh();
                    });

                    // Adding a reference to the selectbox in the object
                    self.cols[colIndex].filterSelectElem = $(select);

                    // Adding the selectbox to the TH container using a customizable method in the options
                    self.options.addFilter(container, select, colIndex);
                }

                if (col.concat) {
                    // Add Concat buttons

                    var el = document.createElement(self.options.concatBtnElement);
                    el.className = self.options.concatBtnClass;
                    $(el).attr('data-table-col-index', colIndex);

                    // Action when clicking on Concat
                    $(el).on('click', function() {
                        self.concat($(this).attr('data-table-col-index'), !$(this).hasClass(self.options.concatBtnClassActive));
                        self.refresh();
                    });

                    // Adding a reference to the element in the object
                    self.cols[colIndex].concatButtonElem = $(el);

                    // Adding the element to the TH container using a customizable method in the options
                    self.options.addConcatBtn(container, el, colIndex);
                }

                if (col.hide) {
                    // Add Hide buttons

                    var el = document.createElement(self.options.hideBtnElement);
                    el.className = self.options.hideBtnClass;
                    $(el).attr('data-table-col-index', colIndex);

                    // Action when clicking on Hide
                    $(el).on('click', function() {
                        self.hide($(this).attr('data-table-col-index'), true);
                    });

                    // Adding a reference to the element in the object
                    self.cols[colIndex].hideButtonElem = $(el);

                    // Adding the element to the TH container using a customizable method in the options
                    self.options.addHideBtn(container, el, colIndex);
                }

            }
        }

        function removeControls() {
            for (var i in self.originalElements) {
                $(self.originalElements[i].elem).html(self.originalElements[i].originalContents);
            }
        }

        /*
         %first% First value as-is
         %last% Last value as-is
         %total% Total value with two digits after decimal
         %totalint% Total using integer value
         %average% Average value with two degits after decimal
         %max% Maximum value as-is
         %min% Minimum value as-is
         %count% Number of concatenated rows
         */
        function getFormatResult(value, colIndex, elems) {
            if (value.match('%first%')) {
                var replacement = getVal($($(elems)[0]).children()[colIndex]);
                value = value.replace('%first%', replacement);
            }
            if (value.match('%last%')) {
                var replacement = getVal($($(elems)[elems.length - 1]).children()[colIndex]);
                value = value.replace('%last%', replacement);
            }
            if (value.match('%total%')) {
                var total = 0.0;
                $(elems).each(function() {
                    total += parseFloat(+getVal($(this).children()[colIndex]));
                });
                var replacement = parseFloat(total).toFixed(2);
                value = value.replace('%total%', replacement);
            }
            if (value.match('%totalint%')) {
                var total = 0.0;
                $(elems).each(function() {
                    total += parseFloat(+getVal($(this).children()[colIndex]));
                });
                var replacement = parseInt(total);
                value = value.replace('%totalint%', replacement);
            }
            if (value.match('%average%')) {
                var total = 0.0;
                $(elems).each(function() {
                    total += parseFloat(+getVal($(this).children()[colIndex]));
                });
                var replacement = (total / elems.length).toFixed(2);
                value = value.replace('%average%', replacement);
            }
            if (value.match('%max%')) {
                var max = getVal($($(elems)[0]).children()[colIndex]);
                $(elems).each(function() {
                    var val = parseFloat(+getVal($(this).children()[colIndex]));
                    if (+val > +max) {
                        max = val;
                    }
                });
                var replacement = max;
                value = value.replace('%max%', replacement);
            }
            if (value.match('%min%')) {
                var min = getVal($($(elems)[0]).children()[colIndex]);
                $(elems).each(function() {
                    var val = parseFloat(+getVal($(this).children()[colIndex]));
                    if (+val < +min) {
                        min = val;
                    }
                });
                var replacement = min;
                value = value.replace('%min%', replacement);
            }
            if (value.match('%count%')) {
                var replacement = elems.length;
                value = value.replace('%count%', replacement);
            }

            return value;
        }

        //////////////////////////////////////////////////////////////////////////////
        // Runtime methods

        /**
         * Sort rows
         * @param int Column Index
         * @param bool Descendant Order
         * @returns $.jDataTable
         */
        this.sort = function(colIndex, desc) {

            if (self.cols[colIndex].sortButtonElem) {
                // Remove active classes
                self.table.find(self.options.headRowSelector).find(self.options.headColsSelector)
                    .find('.' + self.options.sortingArrowClassActive)
                    .removeClass(self.options.sortingArrowClassActive);

                // Set active class
                self.cols[colIndex].sortButtonElem.addClass(self.options.sortingArrowClassActive);

                if (desc) {
                    self.cols[colIndex].sortButtonElem.removeClass(self.options.sortingArrowClassAsc);
                    self.cols[colIndex].sortButtonElem.addClass(self.options.sortingArrowClassDesc);
                } else {
                    self.cols[colIndex].sortButtonElem.removeClass(self.options.sortingArrowClassDesc);
                    self.cols[colIndex].sortButtonElem.addClass(self.options.sortingArrowClassAsc);
                }
            }

            // Set sorting
            var i = self.sortContainer.find('[data-sort-col=' + colIndex + ']');
            if (i[0]) {
                i.attr('data-sort-desc', desc ? 1 : 0);
                self.sortContainer.prepend(i);
            }

            // Call customizable action
            self.options.onAfterSort(desc ? true : false, self.cols[colIndex].sortButtonElem, colIndex);

            return this;
        };

        /**
         * Filter rows
         * @param int Column Index
         * @param string Value to filter by
         * @returns $.jDataTable
         */
        this.filter = function(colIndex, value) {

            // Set current value on filter element
            if (self.cols[colIndex].filterSelectElem) {
                self.cols[colIndex].filterSelectElem.val(value);
            }

            if (value === '') {
                // Unfilter for this column
                self.table.find(self.options.bodyRowSelector)
                    .filter('.' + self.options.filteredClass + '-' + colIndex)
                    .removeClass(self.options.filteredClass + '-' + colIndex);

            } else {
                // Filter by value
                self.table.find(self.options.bodyRowSelector).each(function() {
                    var row = $(this);
                    row.removeClass(self.options.filteredClass + '-' + colIndex);
                    row.find(self.options.bodyColsSelector)
                        .filter(':not(.' + self.options.concatResultClass + ')')
                        .each(function(bodyColIndex) {
                            if (+bodyColIndex === +colIndex) {
                                if (value.toLowerCase() !== getVal(this).toLowerCase()) {
                                    row.addClass(self.options.filteredClass + '-' + colIndex);
                                }
                            }
                        });
                });
            }

            // Call customizable action
            self.options.onAfterFilterChange(value, self.cols[colIndex].filterSelectElem, colIndex);

            return this;
        };

        /**
         * Concatenate rows that have the same value on a given column
         * @param int Column Index
         * @param bool Active (whetter Concatenation is active or not)
         * @returns $.jDataTable
         */
        this.concat = function(colIndex, active) {
            if (self.cols[colIndex].concatButtonElem) {

                // Remove concat-active classes on all columns
                self.table.find(self.options.headRowSelector).find(self.options.headColsSelector)
                    .find('.' + self.options.concatBtnClassActive)
                    .removeClass(self.options.concatBtnClassActive);

                if (active) {
                    // Set concat-active class on current concatenating column
                    self.cols[colIndex].concatButtonElem.addClass(self.options.concatBtnClassActive);
                }
            }

            // Set Concat Column Index
            self.concatColumn = active ? colIndex : false;

            // Call customizable action
            self.options.onAfterConcat(active, self.cols[colIndex].concatButtonElem, colIndex);

            return this;
        };

        /**
         * Hides entire column
         * @param int Column Index
         *      (null, undefined or empty string to unhide ALL columns, with second param undefined or false)
         * @param bool Active (whetter Hiding is active or not)
         * @returns $.jDataTable
         */
        this.hide = function(colIndex, active) {
            if ((colIndex === null || colIndex === '' || typeof colIndex === 'undefined') && !active) {
                // Unhide all columns
                self.table.find('.' + self.options.hideColClass).removeClass(self.options.hideColClass);

            } else {


                // Hide head columns
                self.table.find(self.options.headRowSelector).each(function() {
                    $(this).find(self.options.headColsSelector)
                        .each(function(index) {
                            if (+index === +colIndex) {
                                if (active) {
                                    $(this).addClass(self.options.hideColClass);
                                } else {
                                    $(this).removeClass(self.options.hideColClass);
                                }
                            }
                        });
                });

                // Hide body columns
                self.table.find(self.options.bodyRowSelector).each(function() {
                    $(this).find(self.options.bodyColsSelector)
                        .each(function(index) {
                            if (+index === +colIndex) {
                                if (active) {
                                    $(this).addClass(self.options.hideColClass);
                                } else {
                                    $(this).removeClass(self.options.hideColClass);
                                }
                            }
                        });
                });

                // Hide footer columns
                self.table.find(self.options.footRowSelector).each(function() {
                    $(this).find(self.options.footColsSelector)
                        .each(function(index) {
                            if (+index === +colIndex) {
                                if (active) {
                                    $(this).addClass(self.options.hideColClass);
                                } else {
                                    $(this).removeClass(self.options.hideColClass);
                                }
                            }
                        });
                });

            }
            return this;
        };

        /**
         * Limit the number of visible rows
         * @param int Number of rows to show
         * @returns $.jDataTable
         */
        this.limit = function(nbRows) {

            // Set limit
            self.limitRows = nbRows;

            return this;
        };

        /**
         * Refresh Rows
         * @returns $.jDataTable
         */
        this.refreshRows = function() {
            self.table.find(self.options.bodyRowSelector).each(function() {

                // Remove concatenating result rows
                if ($(this).hasClass(self.options.concatResultClass)) {
                    $(this).remove();
                    return;
                }

                // Hide filtered results
                if (this.className.match(self.options.filteredClass + '-')) {
                    $(this).addClass(self.options.filteredClass);
                } else {
                    $(this).removeClass(self.options.filteredClass);
                }

                // Unhide hidden rows for limit
                $(this).removeClass(self.options.hideRowClass);

            });

            // Hide concatenated rows
            if (self.concatColumn !== false) {

                // Keep track of all concatenated rows associated with their concat column value
                var rows = new Object();
                var valuesList = new Array(); // Contains the list of each distinct value

                // Loop through each row
                self.table.find(self.options.bodyRowSelector).each(function() {
                    // Add concat class to all rows
                    $(this).addClass(self.options.concatClass);

                    // Get value from the concatenating column
                    var value = getVal($(this).find(self.options.bodyColsSelector)[self.concatColumn]).toLowerCase();

                    // Create concatenating row if does not already exist
                    if (typeof rows[value] === 'undefined') {
                        rows[value] = {
                            value : value,
                            lastElement : null,
                            elems : []
                        };
                        valuesList.push(value);
                    }

                    // Add row to concatenating rows array
                    rows[value].lastElement = this;
                    rows[value].elems.push(this);
                });

                // For each different value, get concatenated rows
                for (var i in valuesList) {
                    var value = valuesList[i];

                    // Create Concatenating result row from last element
                    var resultRowElem = $(rows[value].lastElement).clone();

                    // Change classes so the new result row is visible
                    $(resultRowElem).removeClass(self.options.concatClass).addClass(self.options.concatResultClass);

                    // For each column in the resulting row
                    $(resultRowElem).find(self.options.bodyColsSelector).each(function(colIndex) {

                        // Only change value for columns other than the concatenating one
                        if (+self.concatColumn !== +colIndex) {

                            // Get value format from column configuration
                            var val = self.cols[colIndex].onconcat;

                            // Parse formated value
                            val = getFormatResult(val, colIndex, rows[value].elems);

                            // Parsing resulting value using customizable function from the options
                            val = self.options.concatResult($(this), val, value, colIndex, rows[value].elems);

                            // Set final value to resulting column content if not null or false
                            if (val !== false && val !== null) {
                                $(this).html(val);
                            }
                        }
                    });

                    // Insert new result row after the last concatenated element with the same value
                    $(resultRowElem).insertAfter($(rows[value].lastElement));
                }

            } else {
                // Remove concat class from all rows
                self.table.find(self.options.bodyRowSelector).removeClass(self.options.concatClass);
            }

            // Sort all visible rows
            self.table.find(self.options.bodyRowSelector).filter(':visible').sort(function(a, b) {

                // Get sorting colums by sorting priority
                var columns = self.sortContainer.find('i');

                // Loop through sorting columns priority
                for (var i = 0; i < columns.length; i++) {
                    var colIndex = +$(columns[i]).attr('data-sort-col');
                    var sort = self.cols[colIndex].sort;
                    var desc = +$(columns[i]).attr('data-sort-desc') === 1 ? true : false;
                    var asc = !desc;

                    // Get two column values for comparison
                    var valA = getVal($(a).find(self.options.bodyColsSelector)[colIndex]);
                    var valB = getVal($(b).find(self.options.bodyColsSelector)[colIndex]);

                    // Format values correctly for sorting
                    switch (sort) {
                        case 'text' :
                            valA = valA.toLowerCase();
                            valB = valB.toLowerCase();
                            break;
                        case 'number' :
                            valA = parseFloat(valA);
                            valB = parseFloat(valB);
                            break;
                        case 'date' :
                            valA = getTimestamp(valA);
                            valB = getTimestamp(valB);
                            break;
                    }

                    // If values are equal, continue in the loop and try sorting by previous sorting column
                    if (valA === valB) {
                        continue;
                    }

                    // Sort values
                    if (valA < valB) {
                        return asc ? -1 : 1;
                    }
                    if (valA > valB) {
                        return desc ? -1 : 1;
                    }
                }
            }).appendTo(self.table.find(self.options.bodyRowSelector).parent());

            // If a row limit is set...
            if (self.limitRows) {
                // Count visible rows and Hide if passed limit
                self.table.find(self.options.bodyRowSelector).filter(':visible').each(function(i) {
                    if (i >= self.limitRows) {
                        $(this).addClass(self.options.hideRowClass);
                    }
                });
            }

            return this;
        };

        /**
         * Render footer values
         * @returns $.jDataTable
         */
        this.renderFooter = function() {

            // Get all rows, without concatenation, excluding filtered and hidden/limited rows
            var elems = self.table.find(self.options.bodyRowSelector)
                .filter(':not(.' + self.options.concatResultClass + ')')
                .filter(':not(.' + self.options.filteredClass + ')')
                .filter(':not(.' + self.options.hideRowClass + ')');

            // Loop through columns
            for (var colIndex in self.cols) {

                // Loop through footers
                for (var footerId in self.cols[colIndex].footers) {

                    // Get formated value for current footer and column
                    var val = self.cols[colIndex].footers[footerId].val;

                    // Get footer TD/TH element
                    var elem = self.cols[colIndex].footers[footerId].elem;

                    // If we specified a formated value for this column in this footer
                    if (val !== false) {

                        // Parse formated value
                        val = getFormatResult(val, colIndex, elems);

                        // Parsing resulting value using customizable function from the options
                        val = self.options.footerResult(elem, val, colIndex, elems);

                        // Set final value to footer column content if not null or false
                        if (val !== false && val !== null) {
                            $(elem).html(val);
                        }
                    }
                }
            }

            return this;
        };

        /**
         * Render table
         * @returns $.jDataTable
         */
        this.render = function() {
            this.refreshRows();
            this.renderFooter();
            self.options.onAfterRender(self);
            return this;
        };

        /**
         * Refresh table
         * @returns $.jDataTable
         */
        this.refresh = function() {
            this.refreshRows();
            this.renderFooter();
            self.options.onAfterRefresh(self);
            return this;
        };

        //////////////////////////////////////////////////////////////////////////////
        // Constructor
        return this.bind();
        //////////////////////////////////////////////////////////////////////////////
    };
})(jQuery);
